/**
 * @file JSONWriter.cpp
 */
#include "JSONWriter.h"
#include "JSONException.h"
#include "../common/CppMemPool.h"
#include "../common/UtilFuncT.h"
#include "../exceptions/OutOfBoundsException.h"
#include "../convert/Convert.h"
#include "../text/UTFCharConv.h"

using namespace cppflib::io;
using namespace cppflib::collections;
using namespace cppflib::exceptions;
using namespace cppflib::util;
using namespace cppflib::lang;

namespace cppflib
{

namespace json
{

//! JSON object open/close bracket
PRIVATE const wc_t st_objectOpen = _S('{'); 
PRIVATE const wc_t st_objectClose = _S('}'); 
//! JSON array open/close bracket
PRIVATE const wc_t st_arrayOpen = _S('[');
PRIVATE const wc_t st_arrayClose = _S(']'); 

/**
 *  state of a JSONWriter
 */
typedef enum _writerstate
{
   WS_UNKNOWN = 0x01,

   WS_DOCSTART = 0x02,         ///< document start
   WS_OBJECTSTART = 0x04,      ///< object start
   WS_KEY = 0x08,              ///< write key
   WS_VALUE = 0x10,            ///< write value
   WS_OBJECTEND = 0x20,        ///< object end
   WS_ARRAYSTART = 0x40,       ///< array start
   WS_ARRAYEND = 0x80,         ///< array end
   WS_DOCEND = 0x100,          ///< document end
} writerstate_t;

JSONWriter::JSONWriter(void)
{
   this->pWriterState = NULL;
   this->pObjArrayStack = NULL;
   this->pOutputWriter = NULL;
   this->pObjectNameStack = NULL;
   this->pTempCharBuf = NULL;
}

/**
 *  @param [in] pOutputWriter -- the writer to ouput JSON text
 *  @param [in] isIndent -- true to indent object and array on output
 *  @param [in] closeOutputWriter -- true to close the writer, too, if the 'Close' method of this JSONWriter is called.
 */
JSONWriter::JSONWriter(Writer *pOutputWriter, bool isIndent, bool closeOutputWriter)
{
   const int TEMPBUFSIZE = 128;

   this->indent = isIndent;
   this->closeOutputWriter = closeOutputWriter;

   this->pOutputWriter = pOutputWriter;
   cppmempool::Retain(this->pOutputWriter);
   this->pObjArrayStack = new StackWChar(false); // a stack to record the open brackets of JSON objects and arrays
   cppmempool::Retain(this->pObjArrayStack);
   this->pWriterState = new BitAttributeT<enumforward_t>(); // actually writerstate_t
   cppmempool::Retain(this->pWriterState);
   this->pWriterState->Replace(WS_DOCSTART);
   this->pObjectNameStack = new StackCppObjT<HashtableT<CFString, Boolean> >(false);
   cppmempool::Retain(this->pObjectNameStack);
   this->pTempCharBuf = new ArrayWChar(TEMPBUFSIZE);  // big enough to hold 2 wide characters (surrogate pairs)
   cppmempool::Retain(this->pTempCharBuf);
}

JSONWriter::~JSONWriter(void)
{
   Close();
}

/**
 *  To check if state of the writer is legal or not
 *
 *  @param [in] nextState -- next expected state
 */
PRIVATE void __CheckWriterState(BitAttributeT<enumforward_t> &curState, writerstate_t nextState)
{
   if (curState.IsAttrSet(WS_DOCSTART)) {
      if (nextState != WS_OBJECTSTART) 
         throw JSONException(_S("Illegal call after starting a JSON document"));
   }
   else if (curState.IsAttrSet(WS_OBJECTSTART)) {
      if (nextState != WS_KEY && nextState != WS_OBJECTEND) 
         throw JSONException(_S("Illegal call after starting a JSON object"));
   }
   else if (curState.IsAttrSet(WS_KEY)) {
      if (nextState != WS_VALUE && nextState != WS_OBJECTSTART && nextState != WS_ARRAYSTART) 
         throw JSONException(_S("Illegal call after writing a key"));
   }
   else if (curState.IsAttrSet(WS_VALUE)) {
      if (nextState != WS_VALUE && nextState != WS_KEY && 
          nextState != WS_OBJECTSTART && nextState != WS_ARRAYSTART &&
          nextState != WS_OBJECTEND && nextState != WS_ARRAYEND) 
         throw JSONException(_S("Illegal call after writing a value"));
   }
   else if (curState.IsAttrSet(WS_OBJECTEND)) {
      if (nextState != WS_OBJECTSTART && nextState != WS_ARRAYSTART && 
          nextState != WS_KEY && nextState != WS_VALUE && 
          nextState != WS_OBJECTEND && nextState != WS_ARRAYEND) 
         throw JSONException(_S("Illegal call after ending a JSON object"));
   }
   else if (curState.IsAttrSet(WS_ARRAYSTART)) {
      if (nextState != WS_ARRAYEND && nextState != WS_VALUE && 
          nextState != WS_OBJECTSTART && nextState != WS_ARRAYSTART) 
         throw JSONException(_S("Illegal call after starting a JSON array"));
   }
   else if (curState.IsAttrSet(WS_ARRAYEND)) {
      if (nextState != WS_OBJECTSTART && nextState != WS_ARRAYSTART && 
          nextState != WS_KEY && nextState != WS_VALUE && 
          nextState != WS_OBJECTEND && nextState != WS_ARRAYEND) 
         throw JSONException(_S("Illegal call after ending a JSON array"));
   }
   else if (curState.IsAttrSet(WS_DOCEND)) {
      throw JSONException(_S("Illegal call after ending a JSON document"));
   }
   else {
      throw JSONException(_S("JSONWriter in unknown state"));
   }
}

void JSONWriter::WriteNewline(bool forceWrite)
{
   if (forceWrite || this->indent) {
      pOutputWriter->Write(_S('\r'));
      pOutputWriter->Write(_S('\n'));
   }
}

void JSONWriter::WriteIndentSpaces(bool isEndObjArray)
{
   if (this->indent) {
      int indentNum = static_cast<int>(pObjArrayStack->GetCount());
      if (isEndObjArray) --indentNum;  // ending tag should be one less
        
      for (int i = 0; i < indentNum; ++i) {
         pOutputWriter->Write(_S(' '));
         pOutputWriter->Write(_S(' '));
         pOutputWriter->Write(_S(' '));
      }
   }
}

void JSONWriter::WriteComma()
{
   if (pWriterState->IsAttrSet(WS_VALUE) || pWriterState->IsAttrSet(WS_OBJECTEND) || 
       pWriterState->IsAttrSet(WS_ARRAYEND)) {

      pOutputWriter->Write(_S(','));
      WriteNewline();
   }
}

PRIVATE void __FlushBufferAndWriteEscapeChar(ArrayWChar &buf, Writer &w, 
                                             int &tempBufPtr, wc_t char1, wc_t char2)
{
   // flush buffer first
   if (tempBufPtr > 0) {
      w.Write(buf, 0, tempBufPtr);
      tempBufPtr = 0;
   }

   // write escaped char
   buf[0] = (char1 == 0) ? _S('\\') : char1; 
   buf[1] = char2; 
   w.Write(buf, 0, 2); 
}

PRIVATE void __FlushBufferIfFull(ArrayWChar &buf, Writer &w, int &tempBufPtr, bool forceFlush)
{
   if ((forceFlush && tempBufPtr > 0) || tempBufPtr == static_cast<int>(buf.GetSize())) {
      w.Write(buf, 0, tempBufPtr);
      tempBufPtr = 0;
   }
}

void JSONWriter::WriteStringValue(pcwstr_t str, int offset, int count)
{
   int tempBufPtr = 0;  // buffer pointer for pTempCharBuf
   int end = offset + count;

   pOutputWriter->Write(_S('"'));
   
   for (int i = offset; i < end; ++i) {
      wc_t c = str[i];

      switch (c) {
         case _S('"'): 
         case _S('\\'): 
         case _S('/'):
            __FlushBufferAndWriteEscapeChar(*pTempCharBuf, *pOutputWriter, tempBufPtr, 0, c);
            break;
         case _S('\f'): 
            __FlushBufferAndWriteEscapeChar(*pTempCharBuf, *pOutputWriter, tempBufPtr, 0, _S('f'));
            break;
         case _S('\n'): 
            __FlushBufferAndWriteEscapeChar(*pTempCharBuf, *pOutputWriter, tempBufPtr, 0, _S('n'));
            break;
         case _S('\r'): 
            __FlushBufferAndWriteEscapeChar(*pTempCharBuf, *pOutputWriter, tempBufPtr, 0, _S('r'));
            break;
         case _S('\t'): 
            __FlushBufferAndWriteEscapeChar(*pTempCharBuf, *pOutputWriter, tempBufPtr, 0, _S('t'));
            break;
         default:
            if (text::IsUTF16HighSurrogate(c)) { // surrogate pairs
               if (i + 1 >= end)
                  break;

               wc_t tempC = c;
               c = str[++i];
               if (!text::IsUTF16LowSurrogate(c))
                  break;

               __FlushBufferAndWriteEscapeChar(*pTempCharBuf, *pOutputWriter, tempBufPtr, 
                                               tempC, c);
            }
            else {
               __FlushBufferIfFull(*pTempCharBuf, *pOutputWriter, tempBufPtr, false);
               (*pTempCharBuf)[tempBufPtr++] = c;
            }
            break;
      }
   }

   // flush remaining chars if any
   __FlushBufferIfFull(*pTempCharBuf, *pOutputWriter, tempBufPtr, true);

   pOutputWriter->Write(_S('"'));
}

void JSONWriter::Close()
{
   if (pOutputWriter) {
      if (closeOutputWriter) 
         pOutputWriter->Close();
      cppmempool::Release(pOutputWriter);
      pOutputWriter = NULL;
   }

   if (pObjArrayStack) {
      cppmempool::Release(pObjArrayStack);
      pObjArrayStack = NULL;
   }

   if (pWriterState) {
      cppmempool::Release(pWriterState);
      pWriterState = NULL;
   }

   if (pObjectNameStack) {
      cppmempool::Release(pObjectNameStack);
      pObjectNameStack = NULL;
   }

   if (pTempCharBuf) {
      cppmempool::Release(pTempCharBuf);
      pTempCharBuf = NULL;
   }
}

void JSONWriter::Flush()
{
   pOutputWriter->Flush();
}

/**
 *  Object starts
 */
JSONWriter & JSONWriter::Object()
{
   __CheckWriterState(*pWriterState, WS_OBJECTSTART);

   WriteComma();  // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();
   pOutputWriter->Write(st_objectOpen);
   WriteNewline();

   pObjArrayStack->Push(st_objectOpen);
   pObjectNameStack->Push(new HashtableT<CFString, Boolean>());
   pWriterState->Replace(WS_OBJECTSTART);

   return *this;
}

/**
 *  Object ends
 */
JSONWriter & JSONWriter::EndObject()
{
   __CheckWriterState(*pWriterState, WS_OBJECTEND);
   if (pObjArrayStack->GetCount() == 0)
      throw JSONException(_S("No more object to end"));
   if (pObjArrayStack->Peek() != st_objectOpen)
      throw JSONException(_S("Object closing does not match object opening"));

   if (!pWriterState->IsAttrSet(WS_OBJECTSTART))
      WriteNewline();
   WriteIndentSpaces(true);
   pOutputWriter->Write(st_objectClose);

   pObjArrayStack->Pop();
   if (pObjectNameStack->PopAndRemove() == 0)
      pWriterState->Replace(WS_DOCEND);
   else
      pWriterState->Replace(WS_OBJECTEND);

   return *this;
}

/**
 *  Array starts 
 */
JSONWriter & JSONWriter::Array()
{
   __CheckWriterState(*pWriterState, WS_ARRAYSTART);

   WriteComma(); // write comma for previous value
   WriteNewline();
   WriteIndentSpaces();
   pOutputWriter->Write(st_arrayOpen);
   WriteNewline();

   pObjArrayStack->Push(st_arrayOpen);
   pWriterState->Replace(WS_ARRAYSTART);

   return *this;
}

/**
 *  Array ends
 */
JSONWriter & JSONWriter::EndArray()
{
   __CheckWriterState(*pWriterState, WS_ARRAYEND);
   if (pObjArrayStack->GetCount() == 0)
      throw JSONException(_S("No more array to end"));
   if (pObjArrayStack->Peek() != st_arrayOpen)
      throw JSONException(_S("Array closing does not match array opening"));

   if (!pWriterState->IsAttrSet(WS_ARRAYSTART))
      WriteNewline();
   WriteIndentSpaces(true);
   pOutputWriter->Write(st_arrayClose);

   pObjArrayStack->Pop();
   pWriterState->Replace(WS_ARRAYEND);

   return *this;
}

/**
 *  Write object key 
 */
JSONWriter & JSONWriter::Key(const CFString &key)
{
   __CheckWriterState(*pWriterState, WS_KEY);

   if (key.IsEmpty())
      throw JSONException(_S("Key is empty"));
   if (pObjArrayStack->Peek() != st_objectOpen)
      throw JSONException(_S("Key is allowed only within an object"));
   HashtableT<CFString, Boolean> *pCurObjNameTbl = pObjectNameStack->Peek();
   if (pCurObjNameTbl->ContainsKey(key))
      throw JSONException(CFString::SFormat(_S("Duplicated key (\"%s\") in the same object (nested level: %d)"), static_cast<pcwstr_t>(key), pObjectNameStack->GetCount() - 1));

   WriteComma();
   WriteIndentSpaces();
   WriteStringValue(key, 0, key.GetLength());
   pOutputWriter->Write(_S(':'));  // write colon after key

   pCurObjNameTbl->Set(new CFString(key), new Boolean(true));
   pWriterState->Replace(WS_KEY);

   return *this;
}

PRIVATE void __CheckValueAfterValue(BitAttributeT<enumforward_t> &curState, StackWChar &objArrayStack)
{
   if (curState.IsAttrSet(WS_VALUE) && objArrayStack.Peek() != st_arrayOpen)
      throw JSONException(_S("Consecutive values are allowed only within an array"));
}

/**
 *  string value
 */
JSONWriter & JSONWriter::Value(const CFString &value)
{
   Value(value, 0, value.GetLength());
   return *this;
}

/**
 *  string value
 */
JSONWriter & JSONWriter::Value(const CFString &value, int offset, int count)
{
   __CheckWriterState(*pWriterState, WS_VALUE);
   __CheckValueAfterValue(*pWriterState, *pObjArrayStack);
   
   if (count < 0 || offset < 0 || offset >= value.GetLength())
      throw OutOfBoundsException(_S("Writing value out of its size"));

   // use the smaller one
   count = cppflib::Min<int>(count, value.GetLength() - offset);   

   WriteComma(); // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();

   WriteStringValue(value, offset, count);

   pWriterState->Replace(WS_VALUE);

   return *this;
}

/**
 *  wide char array value 
 */
JSONWriter & JSONWriter::Value(ArrayWChar &value)
{
   Value(value, 0, static_cast<int>(value.GetSize()));

   return *this;
}

/**
 *  wide char array value
 */
JSONWriter & JSONWriter::Value(ArrayWChar &value, int offset, int count)
{
   __CheckWriterState(*pWriterState, WS_VALUE);
   __CheckValueAfterValue(*pWriterState, *pObjArrayStack);

   if (count < 0 || offset < 0 || offset >= static_cast<int>(value.GetSize()))
      throw OutOfBoundsException(_S("Writing value out of its size"));

   // use the smaller one
   count = cppflib::Min<int>(count, value.GetSize() - offset);   

   WriteComma(); // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();

   WriteStringValue(value.GetRawPtr(), offset, count);

   pWriterState->Replace(WS_VALUE);

   return *this;
}

/**
 *  boolean value
 */
JSONWriter & JSONWriter::Value(bool v)
{
   __CheckWriterState(*pWriterState, WS_VALUE);
   __CheckValueAfterValue(*pWriterState, *pObjArrayStack);

   WriteComma(); // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();

   if (v)
      pOutputWriter->Write(CFString(_S("true")));
   else
      pOutputWriter->Write(CFString(_S("false")));

   pWriterState->Replace(WS_VALUE);

   return *this;
}

/**
 *  double value
 */
JSONWriter & JSONWriter::Value(double v)
{
   __CheckWriterState(*pWriterState, WS_VALUE);
   __CheckValueAfterValue(*pWriterState, *pObjArrayStack);

   WriteComma(); // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();

   pOutputWriter->Write(convert::ToString(v));
   
   pWriterState->Replace(WS_VALUE);

   return *this;
}

/**
 *  integer value
 */
JSONWriter & JSONWriter::Value(int v)
{
   __CheckWriterState(*pWriterState, WS_VALUE);
   __CheckValueAfterValue(*pWriterState, *pObjArrayStack);

   WriteComma(); // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();

   pOutputWriter->Write(convert::ToString(v));
   
   pWriterState->Replace(WS_VALUE);

   return *this;
}

/**
 *  long int value
 */
JSONWriter & JSONWriter::Value(i64_t v)
{
   __CheckWriterState(*pWriterState, WS_VALUE);
   __CheckValueAfterValue(*pWriterState, *pObjArrayStack);

   WriteComma(); // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();

   pOutputWriter->Write(convert::ToString(v));
   
   pWriterState->Replace(WS_VALUE);

   return *this;
}

/**
 *  Write 'null' value
 */
JSONWriter & JSONWriter::ValueNull()
{
   __CheckWriterState(*pWriterState, WS_VALUE);
   __CheckValueAfterValue(*pWriterState, *pObjArrayStack);

   WriteComma(); // write comma for previous value
   if (!pWriterState->IsAttrSet(WS_KEY))
      WriteIndentSpaces();

   pOutputWriter->Write(CFString(_S("null")));
   
   pWriterState->Replace(WS_VALUE);

   return *this;
}

/**
 *  Write comment (no state is changed)
 */
JSONWriter & JSONWriter::Comment(const CFString &comment)
{
   if (pWriterState->IsAttrSet(WS_DOCSTART) || pWriterState->IsAttrSet(WS_DOCEND) || pWriterState->IsAttrSet(WS_UNKNOWN))
      throw JSONException(_S("Can't write comment before or after JSON text"));

   if (comment.Find(_S("*/")) >= 0)
      throw JSONException(_S("String inside comment cannot contain \"*/\""));

   WriteNewline(true);  //  must force to write newline before and after comments
                        //+ so that this comment won't be adjacent to key or values or other JSON elements
   WriteIndentSpaces();
   pOutputWriter->Write(_S('/'));
   pOutputWriter->Write(_S('*'));
   pOutputWriter->Write(_S(' '));
   pOutputWriter->Write(comment);
   pOutputWriter->Write(_S(' '));
   pOutputWriter->Write(_S('*'));
   pOutputWriter->Write(_S('/'));

   WriteNewline(true);

   return *this;
}

/**
 *  To end the JSON document -- write all closing brackets
 */
void JSONWriter::EndDocument()
{
   if (this->pWriterState->IsAttrSet(WS_DOCSTART))
      return;

   while (pObjArrayStack->GetCount() > 0) {
      wc_t topOpenSymbol = pObjArrayStack->Peek();
      if (topOpenSymbol == st_objectOpen)
         EndObject();
      else if (topOpenSymbol == st_arrayOpen)
         EndArray();
   }
}


} // end of namespace json

} // end of namespace cppflib

